/*
Xern Entity Relocation Network
Copyright (C) 2011 Ronie Salgado <roniesalg@gmail.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include <boost/uuid/uuid_generators.hpp>
#include <set>
#include <string.h>

#include "RegionWriter.hpp"
#include "Xern/DataStream.hpp"
#include "Xern/StringTable.hpp"
#include "Xern/RegionFile.hpp"
#include "Xern/RegionMesh.hpp"

namespace MapXern
{
	// Region Object
	class RegionObject
	{
	public:
		RegionObject();
		~RegionObject();

		bool Write(DataStream &out);

		u16 id;
		u16 className;
		u16 name;
		uuid guid;
		Transform transform;
		RegionFileBodyType bodyType;
		float mass;

		struct KeyValue
		{
			u16 key;
			u16 value;
		};
		typedef std::vector<KeyValue> Properties;
		Properties properties;
		std::vector<u16> children;
		std::vector<u16> meshes;
		std::vector<u16> collisionShapes;
	};
	typedef std::vector<boost::shared_ptr<RegionObject> > RegionObjects;

	RegionObject::RegionObject()
	{
		mass = 0.0f;
		bodyType = RFBT_STATIC;
	}

	RegionObject::~RegionObject()
	{
	}

	bool RegionObject::Write(DataStream &out)
	{
		RegionFileObject obj;
		memset(&obj, 0, sizeof(obj));
		obj.className = className;
		obj.name = name;
		for(int i = 0; i < 16; i++)
			obj.uuid[i] = guid.data[i];

		// Store the position
		const Vector3 &pos = transform.GetVector();
		obj.position[0] = pos.x;
		obj.position[1] = pos.y;
		obj.position[2] = pos.z;

		// Store the matrix
		const Matrix3 &mat = transform.GetMatrix();
		obj.matrix[0] = mat.m1m1;
		obj.matrix[1] = mat.m1m2;
		obj.matrix[2] = mat.m1m3;
		obj.matrix[3] = mat.m2m1;
		obj.matrix[4] = mat.m2m2;
		obj.matrix[5] = mat.m2m3;
		obj.matrix[6] = mat.m3m1;
		obj.matrix[7] = mat.m3m2;
		obj.matrix[8] = mat.m3m3;

		// Store the physics data.
		obj.bodyType = bodyType;
		obj.mass = mass;

		// Store the number of children.
		obj.nummeshes = meshes.size();
		obj.numchildren = children.size();
		obj.numproperties = properties.size();
		obj.numcollisionShapes = collisionShapes.size();

		// Write the object header
		out << obj;

		// Write the meshes.
		for(size_t i = 0; i < meshes.size(); i++)
			out << meshes[i];

		// Write the children.
		for(size_t i = 0; i < children.size(); i++)
			out << children[i];

		// Write the properties.
		for(size_t i = 0; i < properties.size(); i++)
			out << properties[i].key << properties[i].value;

		for(size_t i = 0; i < collisionShapes.size(); i++)
			out << collisionShapes[i];

		return true;
	}

	// Region submesh
	class RegionSubMesh
	{
	public:
		RegionSubMesh();
		~RegionSubMesh();

		bool Write(DataStream &out);

		u16 texture;

		typedef std::vector<u16> Indices;
		Indices indices;
	};

	RegionSubMesh::RegionSubMesh()
	{
	}

	RegionSubMesh::~RegionSubMesh()
	{
	}

	bool RegionSubMesh::Write(DataStream &out)
	{
		RegionFileSubMesh submesh;
		memset(&submesh, 0, sizeof(submesh));

		submesh.texture = texture;
		submesh.indexSize = sizeof(indices[0]);
		submesh.numtriangles = indices.size()/3;
		out << submesh;

		for(size_t i = 0; i < indices.size(); i++)
			out << indices[i];

		return true;
	}

	// Vertices
	const int VertexSize = sizeof(float)*16;
	struct Vertex
	{
		Vector3 position;
		Vector3 normal;
		Vector2 texCoord;
		Vector2 lightCoord;
		Vector3 tangent;
		Vector3 bitangent;
	};
	typedef std::vector<Vertex> Vertices;

	DataStream &operator<<(DataStream &out, const Vertex &v)
	{
		out << v.position.x << v.position.y << v.position.z;
		out << v.normal.x << v.normal.y << v.normal.z;
		out << v.texCoord.x << v.texCoord.y;
		out << v.lightCoord.x << v.lightCoord.y;
		out << v.tangent.x << v.tangent.y << v.tangent.z;
		out << v.bitangent.x << v.bitangent.y << v.bitangent.z;
		return out;
	}

	// Region mesh
	class RegionMesh
	{
	public:
		RegionMesh();
		~RegionMesh();

		void BuildMesh();

		bool Write(DataStream &out);

		u16 id;
		uuid guid;
		u16 name;
		typedef std::set<std::string> Textures;
		Textures textures;

		typedef std::vector<boost::shared_ptr<Brush> > Brushes;
		Brushes brushes;

		typedef std::vector<boost::shared_ptr<RegionSubMesh> > SubMeshes;
		SubMeshes submeshes;

		Vertices vertices;
		AABox boundingBox;
		boost::shared_ptr<StringTable> stringTable;
	};
	typedef std::vector<boost::shared_ptr<RegionMesh> > RegionMeshes;

	RegionMesh::RegionMesh()
	{
		id = 0;
	}

	RegionMesh::~RegionMesh()
	{
	}

	inline Vector3 minv(const Vector3 &a, const Vector3 &b)
	{
		Vector3 ret = a;
		if(b.x < ret.x)
			ret.x = b.x;
		if(b.y < ret.y)
			ret.y = b.y;
		if(b.z < ret.z)
			ret.z = b.z;
		return ret;
	}

	inline Vector3 maxv(const Vector3 &a, const Vector3 &b)
	{
		Vector3 ret = a;
		if(b.x > ret.x)
			ret.x = b.x;
		if(b.y > ret.y)
			ret.y = b.y;
		if(b.z > ret.z)
			ret.z = b.z;
		return ret;
	}

	void RegionMesh::BuildMesh()
	{
		// Store the different textures.
		size_t numbrushes = brushes.size();
		for(size_t i = 0; i < numbrushes; i++)
		{
			boost::shared_ptr<Brush> brush = brushes[i];
			if(i == 0) // Use the guid of the first brush.
				guid = brush->id;

			size_t numfaces = brush->faces.size();
			for(size_t j = 0; j < numfaces; j++)
			{
				const BrushFace &face = brush->faces[j];

				if(face.textureName == "tools/noface")
					continue;

				textures.insert(face.textureName);
			}
		}

		// Create the submeshes,
		size_t numsubmeshes = textures.size();
		submeshes.reserve(numsubmeshes);

		Textures::const_iterator it = textures.begin();
		for(; it != textures.end(); it++)
		{
			std::string texture = *it;

			// Create the submesh.
			boost::shared_ptr<RegionSubMesh> submesh(new RegionSubMesh);
			submeshes.push_back(submesh);

			// Set the submesh texture.
			submesh->texture = stringTable->AddString(texture);

			// Iterate though each brush.
			Brushes::const_iterator bit = brushes.begin();
			for(; bit != brushes.end(); bit++)
			{
				boost::shared_ptr<Brush> brush = *bit;

				// Iterate through each face.
				size_t numfaces = brush->faces.size();
				for(size_t i = 0; i < numfaces; i++)
				{
					const BrushFace &face = brush->faces[i];

					// Check the face texture.
					if(face.textureName != texture)
						continue;

					// Prepare the transformation.
					const Transform &trans = brush->localTransform;
					Matrix3 normMat = trans.GetMatrix().Inverse().Transpose();

					// Add the face vertices.
					size_t start = vertices.size();
					Vector3 normal = face.plane.GetNormal();
					for(size_t j = 0; j < face.vertices.size(); j++)
					{
						// Copy and transform the vertex
						Vertex v;
						v.position = trans*face.vertices[j];
						v.normal = normMat*normal;
						v.texCoord = face.texcoords[j];
						v.lightCoord = Vector2(0.0f);
						v.tangent = normMat*face.tangents[j];
						v.bitangent = normMat*face.bitangents[j];
						vertices.push_back(v);
					}

					// Add the face triangles.
					for(size_t j = 0; j < face.indices.size(); j++)
					{
						u32 index = face.indices[j] + start;
						submesh->indices.push_back(index);
					}
				}
			}
		}

		// Calculate the bounding box.
		size_t numvertices = vertices.size();
		if(numvertices > 0)
		{
			boundingBox.min = boundingBox.max = vertices[0].position;

			for(size_t i = 1; i < numvertices; i++)
			{
				const Vector3 &p = vertices[i].position;
				boundingBox.min = minv(boundingBox.min, p);
				boundingBox.max = maxv(boundingBox.max, p);
			}
		}
	}

	bool RegionMesh::Write(DataStream &out)
	{
		RegionFileMesh mesh;
		memset(&mesh, 0, sizeof(mesh));
		mesh.name = name;
		for(int i = 0; i < 16; i++)
			mesh.uuid[i] = guid.data[i];
		mesh.vertexSize = VertexSize;
		mesh.vertexFlags = RMVF_MAPXERN;
		mesh.numsubmeshes = submeshes.size();
		mesh.numvertices = vertices.size();

		mesh.boundingBoxMin[0] = boundingBox.min.x;
		mesh.boundingBoxMin[1] = boundingBox.min.y;
		mesh.boundingBoxMin[2] = boundingBox.min.z;
		mesh.boundingBoxMax[0] = boundingBox.max.x;
		mesh.boundingBoxMax[1] = boundingBox.max.y;
		mesh.boundingBoxMax[2] = boundingBox.max.z;

		out << mesh;

		// Write submeshes.
		for(size_t i = 0; i < submeshes.size(); i++)
			submeshes[i]->Write(out);

		// Write vertices.
		for(size_t i = 0; i < vertices.size(); i++)
			out << vertices[i];

		return true;
	}

	// Region sub shape.
	class RegionCollisionShape;
	struct RegionSubShape
	{
		Transform trans;
		u16 shape;
	};

	typedef std::vector<RegionSubShape> RegionSubShapes;

	// Region collision shape.
	class RegionCollisionShape
	{
	public:
		RegionCollisionShape();
		~RegionCollisionShape();

		bool Write(DataStream &out);
		void AddChild(const Transform &trans, u16 shape);

		RegionFileShapeType type;
		uuid guid;

		// Sphere
		float radius;

		// Box
		AABox box;

		// Convex hull.
		std::vector<Plane> convexHull;

		// Compound shape.
		RegionSubShapes subshapes;
	};

	typedef std::vector<boost::shared_ptr<RegionCollisionShape> >RegionCollisionShapes;

	RegionCollisionShape::RegionCollisionShape()
	{
		type = RFST_SPHERE;
		radius = 0.1f;
	}

	RegionCollisionShape::~RegionCollisionShape()
	{
	}

	bool RegionCollisionShape::Write(DataStream &out)
	{
		RegionFileCollisionShape shape;
		shape.type = type;
		for(int i = 0; i < 16; i++)
			shape.uuid[i] = guid.data[i];

		// Write the shape diferrently according to his type
		switch(type)
		{
		case RFST_SPHERE:
			{
				// Store the radius.
				shape.sphere.radius = radius;

				// Write the shape.
				out << shape;
			}
			break;
		case RFST_BOX:
			{
				// Copy the box.
				shape.box.min[0] = box.min.x;
				shape.box.min[1] = box.min.y;
				shape.box.min[2] = box.min.z;

				shape.box.max[0] = box.max.x;
				shape.box.max[1] = box.max.y;
				shape.box.max[2] = box.max.z;

				// Write the shape
				out << shape;
			}
			break;
		case RFST_CONVEX_HULL:
			{
				// Store the number of planes.
				size_t numplanes = convexHull.size();
				shape.convexHull.numplanes = numplanes;

				// Write the header.
				out << shape;

				// Write the planes.
				for(size_t i = 0; i < numplanes; i++)
				{
					const Plane &p = convexHull[i];
					out << p.a << p.b << p.c << p.d;
				}
			}
			break;
		case RFST_COMPOUND:
			{
				// Store the number of subshapes.
				size_t numsubshapes = subshapes.size();
				shape.compound.numsubshapes = numsubshapes;

				// Write the header.
				out << shape;

				// Write the subshapes.
				for(size_t i = 0; i < numsubshapes; i++)
				{
					RegionSubShape &sub = subshapes[i];
					RegionFileSubCollisionShape fsub;

					// Copy the position.
					const Vector3 &pos = sub.trans.GetVector();
					fsub.position[0] = pos.x;
					fsub.position[1] = pos.y;
					fsub.position[2] = pos.z;

					// Copy the rotation.
					const Quaternion &rot = sub.trans.GetRotation();
					fsub.rotation[0] = rot.w;
					fsub.rotation[1] = rot.x;
					fsub.rotation[2] = rot.y;
					fsub.rotation[3] = rot.z;

					// Copy the child id.
					fsub.shape = sub.shape;

					// Write the subshape.
					out << fsub;
				}
			}
			break;
		}

		return true;
	}

	void RegionCollisionShape::AddChild(const Transform &trans,	u16 shape)
	{
		RegionSubShape s;
		s.trans = trans;
		s.shape = shape;
		subshapes.push_back(s);
	}

	// Region.
	class Region
	{
	public:
		Region();
		~Region();

		// Region building
		void BuildRegion(boost::shared_ptr<Object> root);
		u16 PrepareObject(boost::shared_ptr<Object> object, const Transform &trans);
		void PrepareChildren(boost::shared_ptr<RegionObject> regionObject,
				boost::shared_ptr<RegionMesh> regionMesh,
				boost::shared_ptr<RegionCollisionShape> regionShape,
				boost::shared_ptr<Object> object,
				const Transform &trans);
		u16 CreateObjectShape(boost::shared_ptr<Object> object);

		// Region writing.
		bool Write(const std::string &filename);
		bool WriteObjects(DataStream &out);
		bool WriteMeshes(DataStream &out);
		bool WriteCollisionShapes(DataStream &out);

		RegionObjects objects;
		RegionMeshes meshes;
		RegionCollisionShapes collisionShapes;
		boost::shared_ptr<StringTable> stringTable;
		u16 rootId;
		boost::uuids::random_generator idGenerator;
	};

	Region::Region()
	{
		stringTable.reset(new StringTable);
	}

	Region::~Region()
	{
	}

	void Region::BuildRegion(boost::shared_ptr<Object> root)
	{
		rootId = PrepareObject(root, Transform());
	}

	u16 Region::PrepareObject(boost::shared_ptr<Object> object, const Transform &trans)
	{
		// Create the region object.
		u16 ret = objects.size();
		boost::shared_ptr<RegionObject> regionObject(new RegionObject);
		objects.push_back(regionObject);
		regionObject->id = ret;
		regionObject->guid = object->id;

		// Set the object name.
		regionObject->name = stringTable->AddString(object->name);

		// Set the object transform.
		regionObject->transform = trans * object->transform;

		// Read the object entity.
		boost::shared_ptr<Entity> ent = object->entity;
		if(ent)
		{
			// Set the class name.
			regionObject->className = stringTable->AddString(ent->className);

			// Store the properties.
			Entity::Properties::const_iterator it = ent->properties.begin();
			for(; it != ent->properties.end(); it++)
			{
				RegionObject::KeyValue kv;
				kv.key = stringTable->AddString(it->first);
				kv.value = stringTable->AddString(it->second);
				regionObject->properties.push_back(kv);
			}
		}
		else
		{
			regionObject->className = stringTable->AddString("");
		}

		// If there aren't brushes and children, just return.
		if(!object->brush && object->children.size() == 0)
			return ret;

		// Build the object mesh.
		boost::shared_ptr<RegionMesh> regionMesh(new RegionMesh);
		regionMesh->id = meshes.size();
		regionMesh->name = stringTable->AddString("GenMesh");
		regionMesh->stringTable = stringTable;
		regionObject->meshes.push_back(regionMesh->id);
		meshes.push_back(regionMesh);

		// Create the collision shape.
		boost::shared_ptr<RegionCollisionShape> regionShape(new RegionCollisionShape);
		regionShape->type = RFST_COMPOUND;
		regionShape->guid = idGenerator();
		regionObject->collisionShapes.push_back(collisionShapes.size());
		collisionShapes.push_back(regionShape);

		// Add each brush to the region mesh.
		if(object->brush)
		{
			regionMesh->brushes.push_back(object->brush);
		}

		// Take into account the children.
		PrepareChildren(regionObject, regionMesh, regionShape, object, Transform());

		// Build the actual mesh.
		if(regionMesh->brushes.size() == 0)
		{
			regionObject->meshes.clear();
		}
		else
		{
			regionMesh->BuildMesh();
		}

		// Remove empty meshes and collision objects.
		if(regionMesh->brushes.size() == 0)
			regionObject->meshes.clear();

		if(regionShape->subshapes.size() == 0)
			regionObject->collisionShapes.clear();

		// Return the region object id.
		return ret;
	}

	void Region::PrepareChildren(boost::shared_ptr<RegionObject> regionObject,
			boost::shared_ptr<RegionMesh> regionMesh,
			boost::shared_ptr<RegionCollisionShape> regionShape,
			boost::shared_ptr<Object> object,
			const Transform &trans)
	{
		Object::Children::const_iterator it = object->children.begin();
		for(; it != object->children.end(); it++)
		{
			boost::shared_ptr<Object> child = *it;

			// Check the presence of an entity.
			if(child->entity)
			{
				u16 childId = PrepareObject(child, trans);
				regionObject->children.push_back(childId);
				continue;
			}

			// Add the brush.
			Transform childTrans = trans*child->transform;
			if(child->brush)
			{
				// Add the brush to the mesh.
				child->brush->localTransform = childTrans;
				regionMesh->brushes.push_back(child->brush);
			}

			// Create the child shape.
			u16 shapeId = CreateObjectShape(child);
			regionShape->AddChild(childTrans, shapeId);

			// Continue processing the children.
			PrepareChildren(regionObject, regionMesh, regionShape, child, childTrans);
		}
	}

	u16 Region::CreateObjectShape(boost::shared_ptr<Object> object)
	{
		// Create the collision shape.
		boost::shared_ptr<RegionCollisionShape> shape(new RegionCollisionShape);
		shape->guid = idGenerator();
		u16 ret = collisionShapes.size();
		collisionShapes.push_back(shape);

		// Create a convex hull shape for brushes.
		boost::shared_ptr<Brush> brush = object->brush;
		if(brush)
		{
			// Change the shape type.
			shape->type = RFST_CONVEX_HULL;

			// Store the planes.
			size_t numplanes = brush->faces.size();
			shape->convexHull.reserve(numplanes);
			for(size_t i = 0; i < numplanes; i++)
				shape->convexHull.push_back(brush->faces[i].plane);
		}

		// Return the shape id.
		return ret;
	}

	bool Region::WriteObjects(DataStream &out)
	{
		RegionObjects::iterator it = objects.begin();
		for(; it != objects.end(); it++)
		{
			boost::shared_ptr<RegionObject> object = *it;
			object->Write(out);
		}

		return true;
	}

	bool Region::WriteMeshes(DataStream &out)
	{
		RegionMeshes::iterator it = meshes.begin();
		for(; it != meshes.end(); it++)
		{
			boost::shared_ptr<RegionMesh> mesh = *it;
			mesh->Write(out);
		}

		return true;
	}

	bool Region::WriteCollisionShapes(DataStream &out)
	{
		RegionCollisionShapes::iterator it = collisionShapes.begin();
		for(; it != collisionShapes.end(); it++)
		{
			boost::shared_ptr<RegionCollisionShape> shape = *it;
			shape->Write(out);
		}

		return true;
	}

	bool Region::Write(const std::string &filename)
	{
		// First open the file.
		FILE *file = fopen(filename.c_str(), "wb");
		if(!file)
		{
			perror("Couldn't write region");
			return false;
		}

		// Create the data stream.
		DataStream out(file);

		// Create the header.
		RegionFileHeader header;
		header.signature = RegionSignature;
		header.headerSize = RegionHeaderSize;

		// Write the temporal header.
		out << header;

		// Write the string table.
		header.stringTableOffset = ftell(file);
		stringTable->WriteStringTable(out);
		header.stringTableSize = ftell(file) - header.stringTableOffset;

		// Write the objects.
		header.objectTableOffset = ftell(file);
		WriteObjects(out);
		header.objectTableSize = ftell(file) - header.objectTableOffset;

		// Write the meshes.
		header.meshTableOffset = ftell(file);
		WriteMeshes(out);
		header.meshTableSize = ftell(file) - header.meshTableOffset;

		// Write the collision shapes.
		header.collisionTableOffset = ftell(file);
		WriteCollisionShapes(out);
		header.collisionTableSize = ftell(file) - header.collisionTableOffset;

		// Write the fs.
		header.fsOffset = ftell(file);
		header.fsSize = 0;

		// Write the extension data.
		header.extensionOffset = ftell(file);
		header.extensionSize = 0;

		// Write the root object.
		header.numobjects = objects.size();
		header.rootObject = rootId;

		// Rewrite the header.
		fseek(file, 0, SEEK_SET);
		out << header;

		// Close the file.
		fclose(file);
		return true;
	}

	bool WriteRegion(boost::shared_ptr<Object> root, const std::string &filename)
	{
		// First create a region object.
		Region region;
		region.BuildRegion(root);

		// Then, write the region.
		return region.Write(filename);
	}

};
